﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using NuMvvm.Validation;

namespace NuMvvm
{
    public abstract class ViewModelBase : ObservableObject, INotifyPropertyChanged, INotifyDataErrorInfo
    {
        private readonly Dictionary<string, List<string>> _errors;

        private EventHandler<DataErrorsChangedEventArgs> _errorsChangedEventHandler;

        public ViewModelBase()
        {
            _errors = new Dictionary<string, List<string>>();
        }


        #region Validation

        public bool IsValid
        {
            get
            {
                return !GetErrors().Any();
            }
        }

        protected void AddError(string propertyName, string message)
        {
            List<string> errors = null;
            if (!_errors.TryGetValue(propertyName, out errors))
            {
                errors = new List<string>();
                _errors[propertyName] = errors;
            }
            errors.Add(message);
            if (_errorsChangedEventHandler != null)
            {
                _errorsChangedEventHandler(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        protected void AddError<TProperty>(Expression<Func<TProperty>> propertyExpression, string message)
        {
            if (propertyExpression == null)
            {
                throw new ArgumentNullException("propertyExpression");
            }
            string propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
            AddError(propertyName, message);
        }

        protected void ClearErrors(string propertyName = null)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                _errors.Clear();
            }
            else
            {
                List<string> errors;
                if (_errors.TryGetValue(propertyName, out errors))
                {
                    errors.Clear();
                }
            }
        }

        protected void ClearErrors<TProperty>(Expression<Func<TProperty>> propertyExpression)
        {
            if (propertyExpression == null)
            {
                throw new ArgumentNullException("propertyExpression");
            }
            string propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
            ClearErrors(propertyName);
        }

        protected IEnumerable<string> GetErrors([CallerMemberName] string propertyName = null)
        {
            return ((INotifyDataErrorInfo)this).GetErrors(propertyName).OfType<string>();
        }

        protected IEnumerable<string> GetErrors<TProperty>(Expression<Func<TProperty>> propertyExpression)
        {
            if (propertyExpression == null)
            {
                throw new ArgumentNullException("propertyExpression");
            }
            string propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
            return GetErrors(propertyName);
        }

        public bool Validate()
        {
            bool result = true;
            foreach (var propertyInfo in GetType().GetTypeInfo().DeclaredProperties)
            {
                var value = propertyInfo.GetValue(this);
                result &= ValidateProperty(propertyInfo.Name, value);
                if (!result)
                {
                    return false;
                }
            }
            return result;
        }

        protected bool ValidateProperty([CallerMemberName] string propertyName = null)
        {
            if (propertyName == null)
            {
                throw new ArgumentNullException("propertyName");
            }

            var value = GetPropertyValue(propertyName);
            return ValidatePropertyValue(propertyName, value);
        }

        protected bool ValidateProperty<TProperty>(Expression<Func<TProperty>> propertyExpression)
        {
            if (propertyExpression == null)
            {
                throw new ArgumentNullException("propertyExpression");
            }
            string propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
            return ValidateProperty(propertyName);
        }

        protected bool ValidateProperty<TValue>(string propertyName, TValue value)
        {
            return ValidatePropertyValue(propertyName, value);
        }

        protected bool ValidatePropertyValue<TValue>(TValue value, [CallerMemberName] string propertyName = null)
        {
            return ValidatePropertyValue(propertyName, value);
        }

        protected bool ValidateProperty<TProperty, TValue>(Expression<Func<TProperty>> propertyExpression, TValue value)
        {
            if (propertyExpression == null)
            {
                throw new ArgumentNullException("propertyExpression");
            }
            string propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
            return ValidatePropertyValue(propertyName);
        }

        #endregion

        #region Implements INotifyDataErrorInfo

        public bool HasErrors
        {
            get
            {
                return _errors.Any();
            }
        }

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        IEnumerable INotifyDataErrorInfo.GetErrors(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                return (_errors.Values).ToArray();
            }

            List<string> results = null;
            if(!_errors.TryGetValue(propertyName, out results))
            {
                results = new List<string>();
            }

            return results.ToArray();
        }

        #endregion

        #region Helpers

        private bool ValidatePropertyValue(string propertyName, object value)
        {
            var validationResults = new List<ValidationResult>();
            if (!Validator.TryValidateProperty(value, new ValidationContext(this) { MemberName = propertyName }, validationResults))
            {
                foreach (var error in validationResults.Take(1))    // Take the first error.
                {
                    AddError(propertyName, error.ErrorMessage);
                }
                return false;
            }
            else
            {
                ClearErrors(propertyName);
                return true;
            }
        }

        #endregion
    }
}
